/*
 * Copyright (C) 2012 CyberAgent
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.jarlen.photoedit.filter.base;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.LinkedList;
import java.util.Queue;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView.Renderer;

@SuppressLint("NewApi")
@TargetApi(11)
public class GPUImageRenderer implements Renderer, PreviewCallback
{
	public static final int NO_IMAGE = -1;
	public static final float CUBE[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
			1.0f, 1.0f,};

	private GPUImageFilter mFilter;

	public final Object mSurfaceChangedWaiter = new Object();

	private int mGLTextureId = NO_IMAGE;
	private SurfaceTexture mSurfaceTexture = null;
	private final FloatBuffer mGLCubeBuffer;
	private final FloatBuffer mGLTextureBuffer;
	private IntBuffer mGLRgbBuffer;

	private int mOutputWidth;
	private int mOutputHeight;
	private int mImageWidth;
	private int mImageHeight;
	private int mAddedPadding;

	private final Queue<Runnable> mRunOnDraw;
	private final Queue<Runnable> mRunOnDrawEnd;
	private Rotation mRotation;
	private boolean mFlipHorizontal;
	private boolean mFlipVertical;
	private GPUImage.ScaleType mScaleType = GPUImage.ScaleType.CENTER_CROP;

	public GPUImageRenderer(final GPUImageFilter filter)
	{
		mFilter = filter;
		mRunOnDraw = new LinkedList<Runnable>();
		mRunOnDrawEnd = new LinkedList<Runnable>();

		mGLCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		mGLCubeBuffer.put(CUBE).position(0);

		mGLTextureBuffer = ByteBuffer
				.allocateDirect(
						TextureRotationUtil.TEXTURE_NO_ROTATION.length * 4)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		setRotation(Rotation.NORMAL, false, false);
	}

	@Override
	public void onSurfaceCreated(final GL10 unused, final EGLConfig config)
	{
		GLES20.glClearColor(0, 0, 0, 1);
		GLES20.glDisable(GLES20.GL_DEPTH_TEST);
		mFilter.init();
	}

	@Override
	public void onSurfaceChanged(final GL10 gl, final int width,
			final int height)
	{
		mOutputWidth = width;
		mOutputHeight = height;
		GLES20.glViewport(0, 0, width, height);
		GLES20.glUseProgram(mFilter.getProgram());
		mFilter.onOutputSizeChanged(width, height);
		adjustImageScaling();
		synchronized (mSurfaceChangedWaiter)
		{
			mSurfaceChangedWaiter.notifyAll();
		}
	}

	@SuppressLint("WrongCall")
	@Override
	public void onDrawFrame(final GL10 gl)
	{
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
		runAll(mRunOnDraw);
		mFilter.onDraw(mGLTextureId, mGLCubeBuffer, mGLTextureBuffer);
		runAll(mRunOnDrawEnd);
		if (mSurfaceTexture != null)
		{
			mSurfaceTexture.updateTexImage();
		}
	}

	private void runAll(Queue<Runnable> queue)
	{
		synchronized (queue)
		{
			while (!queue.isEmpty())
			{
				queue.poll().run();
			}
		}
	}

	@Override
	public void onPreviewFrame(final byte[] data, final Camera camera)
	{
		final Size previewSize = camera.getParameters().getPreviewSize();
		if (mGLRgbBuffer == null)
		{
			mGLRgbBuffer = IntBuffer.allocate(previewSize.width
					* previewSize.height);
		}
		if (mRunOnDraw.isEmpty())
		{
			runOnDraw(new Runnable()
			{
				@Override
				public void run()
				{
					YUVDecoder.YUVtoRBGA(data, previewSize.width,
							previewSize.height, mGLRgbBuffer.array());
					mGLTextureId = OpenGlUtils.loadTexture(mGLRgbBuffer,
							previewSize, mGLTextureId);
					camera.addCallbackBuffer(data);

					if (mImageWidth != previewSize.width)
					{
						mImageWidth = previewSize.width;
						mImageHeight = previewSize.height;
						adjustImageScaling();
					}
				}
			});
		}
	}

	public void setUpSurfaceTexture(final Camera camera)
	{
		runOnDraw(new Runnable()
		{
			@Override
			public void run()
			{
				int[] textures = new int[1];
				GLES20.glGenTextures(1, textures, 0);
				mSurfaceTexture = new SurfaceTexture(textures[0]);
				try
				{
					camera.setPreviewTexture(mSurfaceTexture);
					camera.setPreviewCallback(GPUImageRenderer.this);
					camera.startPreview();
				} catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		});
	}

	public void setFilter(final GPUImageFilter filter)
	{
		runOnDraw(new Runnable()
		{

			@Override
			public void run()
			{
				final GPUImageFilter oldFilter = mFilter;
				mFilter = filter;
				if (oldFilter != null)
				{
					oldFilter.destroy();
				}
				mFilter.init();
				GLES20.glUseProgram(mFilter.getProgram());
				mFilter.onOutputSizeChanged(mOutputWidth, mOutputHeight);
			}
		});
	}

	public void deleteImage()
	{
		runOnDraw(new Runnable()
		{

			@Override
			public void run()
			{
				GLES20.glDeleteTextures(1, new int[]{mGLTextureId}, 0);
				mGLTextureId = NO_IMAGE;
			}
		});
	}

	public void setImageBitmap(final Bitmap bitmap)
	{
		setImageBitmap(bitmap, true);
	}

	public void setImageBitmap(final Bitmap bitmap, final boolean recycle)
	{
		if (bitmap == null)
		{
			return;
		}

		runOnDraw(new Runnable()
		{

			@Override
			public void run()
			{
				Bitmap resizedBitmap = null;
				if (bitmap.getWidth() % 2 == 1)
				{
					resizedBitmap = Bitmap.createBitmap(bitmap.getWidth() + 1,
							bitmap.getHeight(), Bitmap.Config.ARGB_8888);
					Canvas can = new Canvas(resizedBitmap);
					can.drawARGB(0x00, 0x00, 0x00, 0x00);
					can.drawBitmap(bitmap, 0, 0, null);
					mAddedPadding = 1;
				} else
				{
					mAddedPadding = 0;
				}

				mGLTextureId = OpenGlUtils.loadTexture(resizedBitmap != null
						? resizedBitmap
						: bitmap, mGLTextureId, recycle);
				if (resizedBitmap != null)
				{
					resizedBitmap.recycle();
				}
				mImageWidth = bitmap.getWidth();
				mImageHeight = bitmap.getHeight();
				adjustImageScaling();
			}
		});
	}

	public void setScaleType(GPUImage.ScaleType scaleType)
	{
		mScaleType = scaleType;
	}

	protected int getFrameWidth()
	{
		return mOutputWidth;
	}

	protected int getFrameHeight()
	{
		return mOutputHeight;
	}

	private void adjustImageScaling()
	{
		float outputWidth = mOutputWidth;
		float outputHeight = mOutputHeight;
		if (mRotation == Rotation.ROTATION_270
				|| mRotation == Rotation.ROTATION_90)
		{
			outputWidth = mOutputHeight;
			outputHeight = mOutputWidth;
		}

		float ratio1 = outputWidth / mImageWidth;
		float ratio2 = outputHeight / mImageHeight;
		float ratioMax = Math.max(ratio1, ratio2);
		int imageWidthNew = Math.round(mImageWidth * ratioMax);
		int imageHeightNew = Math.round(mImageHeight * ratioMax);

		float ratioWidth = imageWidthNew / outputWidth;
		float ratioHeight = imageHeightNew / outputHeight;

		float[] cube = CUBE;
		float[] textureCords = TextureRotationUtil.getRotation(mRotation,
				mFlipHorizontal, mFlipVertical);
		if (mScaleType == GPUImage.ScaleType.CENTER_CROP)
		{
			float distHorizontal = (1 - 1 / ratioWidth) / 2;
			float distVertical = (1 - 1 / ratioHeight) / 2;
			textureCords = new float[]{
					addDistance(textureCords[0], distHorizontal),
					addDistance(textureCords[1], distVertical),
					addDistance(textureCords[2], distHorizontal),
					addDistance(textureCords[3], distVertical),
					addDistance(textureCords[4], distHorizontal),
					addDistance(textureCords[5], distVertical),
					addDistance(textureCords[6], distHorizontal),
					addDistance(textureCords[7], distVertical),};
		} else
		{
			cube = new float[]{CUBE[0] / ratioHeight, CUBE[1] / ratioWidth,
					CUBE[2] / ratioHeight, CUBE[3] / ratioWidth,
					CUBE[4] / ratioHeight, CUBE[5] / ratioWidth,
					CUBE[6] / ratioHeight, CUBE[7] / ratioWidth,};
		}

		mGLCubeBuffer.clear();
		mGLCubeBuffer.put(cube).position(0);
		mGLTextureBuffer.clear();
		mGLTextureBuffer.put(textureCords).position(0);
	}

	private float addDistance(float coordinate, float distance)
	{
		return coordinate == 0.0f ? distance : 1 - distance;
	}

	public void setRotationCamera(final Rotation rotation,
			final boolean flipHorizontal, final boolean flipVertical)
	{
		setRotation(rotation, flipVertical, flipHorizontal);
	}

	public void setRotation(final Rotation rotation)
	{
		mRotation = rotation;
		adjustImageScaling();
	}

	public void setRotation(final Rotation rotation,
			final boolean flipHorizontal, final boolean flipVertical)
	{
		mFlipHorizontal = flipHorizontal;
		mFlipVertical = flipVertical;
		setRotation(rotation);
	}

	public Rotation getRotation()
	{
		return mRotation;
	}

	public boolean isFlippedHorizontally()
	{
		return mFlipHorizontal;
	}

	public boolean isFlippedVertically()
	{
		return mFlipVertical;
	}

	protected void runOnDraw(final Runnable runnable)
	{
		synchronized (mRunOnDraw)
		{
			mRunOnDraw.add(runnable);
		}
	}

	protected void runOnDrawEnd(final Runnable runnable)
	{
		synchronized (mRunOnDrawEnd)
		{
			mRunOnDrawEnd.add(runnable);
		}
	}
}
